﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace nTyrant
{
    /// <summary>
    /// Tokyo Tyrant Client
    /// </summary>
    public class Client
    {
        /// <summary>
        /// yea ummm look at it
        /// </summary>
        const int DEFAULTPORT = 1978;
        /// <summary>
        /// Success code
        /// </summary>
        const byte ERRORSUCCESS = 0;
        /// <summary>
        /// Endpoint for the client.
        /// </summary>
        public IPEndPoint EndPoint { get; private set; }
        /// <summary>
        /// Creates a new client based on an endpoint.
        /// </summary>
        /// <param name="endpoint">endpoint to connect to</param>
        public Client(IPEndPoint endpoint)
        {
            if (null == endpoint) throw new ArgumentNullException("endpoint", "endpoint cannot be null.");
            EndPoint = endpoint;
        }
        /// <summary>
        /// Creates a new client based on the hostname or ip address.
        /// </summary>
        /// <param name="HostNameOrIpAddress">hostname or ip address</param>
        public Client(string HostNameOrIpAddress):this(HostNameOrIpAddress, DEFAULTPORT)
        {

        }
        /// <summary>
        /// Creates a new client based on the hostname or ip address.
        /// </summary>
        /// <param name="HostNameOrIpAddress">hostname or ip address</param>
        /// <param name="Port">Port to connect to</param>
        public Client(string HostNameOrIpAddress, int Port)
        {
            if (string.IsNullOrEmpty(HostNameOrIpAddress)) throw new ArgumentNullException("HostNameOrIpAddress", "HostNameOrIpAddress cannot be null or empty.");
            IPAddress[] addresses = Dns.GetHostAddresses(HostNameOrIpAddress);
            this.EndPoint = new IPEndPoint(addresses[0], Port);
        }


        private enum Commands : byte
        {
            Put = 0x10,
            PutKeep = 0x11,
            PutCat = 0x12,
            PutShl = 0x13,
            PutNR = 0x18,
            Out = 0x20,
            Get = 0x30,
            Mget = 0x31,
            VSiz = 0x38,
            IterInit = 0x50,
            IterNext = 0x51,
            FWMKeys = 0x58,
            AddInt = 0x60,
            AddDouble = 0x61,
            Sync = 0x70,
            Optimize = 0x71,
            Vanish = 0x72
        }

        private const byte STARTBYTE = 0xC8;
        /// <summary>
        /// Method is used to store a value if a value is not there. 
        /// </summary>
        /// <param name="key">key to persist</param>
        /// <param name="value">value to store</param>
        /// <returns>true if successful, false if not</returns>
        public bool PutKeep(byte[] key, byte[] value)
        {
            return Put(key, value, Commands.PutKeep);
        }
        /// <summary>
        /// Method is used to store a value at the supplied key. 
        /// </summary>
        /// <param name="key">key to persist</param>
        /// <param name="value">value to store</param>
        /// <returns>true if successful, false if not</returns>
        public bool Put(byte[] key, byte[] value)
        {
            return Put(key, value, Commands.Put);
        }
        /// <summary>
        /// Method is used to concatenate the value to the right of the current value.
        /// </summary>
        /// <param name="key">key to persist</param>
        /// <param name="value">value to store</param>
        /// <returns>true if successful, false if not</returns>
        public bool PutConcat(byte[] key, byte[] value)
        {
            return Put(key, value, Commands.PutCat);
        }
        /// <summary>
        /// Method is used to concatenate the value to the left of the current value.
        /// </summary>
        /// <param name="key">key to persist</param>
        /// <param name="value">value to store</param>
        /// <returns>true if successful, false if not</returns>
        public bool PutConcatShiftLeft(byte[] key, byte[] value)
        {
            return Put(key, value, Commands.PutShl);
        }
        /// <summary>
        /// Method is used to put a value in the database but not wait on the answer.
        /// </summary>
        /// <param name="key">key to persist</param>
        /// <param name="value">value to store</param>
        public void PutNoReturn(byte[] key, byte[] value)
        {
            Put(key, value, Commands.PutNR);
        }
        /// <summary>
        /// Method is used to delete a key from the database.
        /// </summary>
        /// <param name="key">Key to remove</param>
        /// <returns>true if successful, false if not.</returns>
        public bool Delete(byte[] key)
        {
            if (IsNullOrEmpty(key)) throw new ArgumentNullException("key", "key cannot be null or empty.");
            const int HEADERLENGTH = 6;
            int sendBufferLength = HEADERLENGTH + key.Length;
            byte[] sendBuffer = new byte[sendBufferLength];
            sendBuffer[0] = STARTBYTE;
            sendBuffer[1] = (byte)Commands.Out;
            int index = 2;
            writeInt32(sendBuffer, key.Length, ref index);
            copyArray(sendBuffer, key, ref index);

            byte result;
            using (Connection conn = ConnectionPool.GetConnection(EndPoint))
            {
                conn.Socket.Send(sendBuffer);
                byte[] buffer = new byte[1];
                int length = conn.Socket.Receive(buffer);
                result = buffer[0];
            }
            return ERRORSUCCESS == result;
        }
        /// <summary>
        /// Method is used to get data from the database by key.
        /// </summary>
        /// <param name="key">Key to search for</param>
        /// <param name="data">Data if found. If not found then empty.</param>
        /// <returns>true if success, false if not</returns>
        public bool Get(byte[] key, out byte[] data)
        {
            if (IsNullOrEmpty(key)) throw new ArgumentNullException("key", "key cannot be null or empty.");
            const int HEADERLENGTH = 6;
            int sendBufferLength = HEADERLENGTH + key.Length;
            byte[] sendBuffer = new byte[sendBufferLength];
            sendBuffer[0] = STARTBYTE;
            sendBuffer[1] = (byte)Commands.Get;
            int index = 2;
            writeInt32(sendBuffer, key.Length, ref index);
            copyArray(sendBuffer, key, ref index);

            byte result;
            using (Connection conn = ConnectionPool.GetConnection(EndPoint))
            {
                conn.Socket.Send(sendBuffer);
                byte[] buffer = new byte[5];
                int length = conn.Socket.Receive(buffer);
                result = buffer[0];

                if (ERRORSUCCESS != result)
                {
                    data = new byte[0];
                    return false;
                }

                int resultLength = ToBigEndian(buffer, 1);
                data = new byte[resultLength];
                readData(data, conn, resultLength);
                return true;
            }
        }
        /// <summary>
        /// Method is used to return the size of the of value behind a certain key.
        /// </summary>
        /// <param name="key">Key to search for</param>
        /// <returns>Length of the value.</returns>
        public int GetSize(byte[] key)
        {
            if (IsNullOrEmpty(key)) throw new ArgumentNullException("key", "key cannot be null or empty.");
            const int HEADERLENGTH = 6;
            int sendBufferLength = HEADERLENGTH + key.Length;
            byte[] sendBuffer = new byte[sendBufferLength];
            sendBuffer[0] = STARTBYTE;
            sendBuffer[1] = (byte)Commands.VSiz;
            int index = 2;
            writeInt32(sendBuffer, key.Length, ref index);
            copyArray(sendBuffer, key, ref index);

            byte result;
            using (Connection conn = ConnectionPool.GetConnection(EndPoint))
            {
                conn.Socket.Send(sendBuffer);
                byte[] buffer = new byte[5];
                int length = conn.Socket.Receive(buffer);
                result = buffer[0];

                if (ERRORSUCCESS != result)
                {
                    return -1;
                }

                int resultLength = ToBigEndian(buffer, 1);
                return resultLength;
            }
        }
        /// <summary>
        /// Method is used to iterate through all the keys in the database.
        /// </summary>
        /// <returns>IEnumerable containing keys beloning to this database.</returns>
        public IEnumerable<byte[]> GetIterator()
        {
            byte[] request = new byte[] { STARTBYTE, (byte)Commands.IterInit };
            byte result;
            using (Connection conn = ConnectionPool.GetConnection(EndPoint))
            {
                conn.Socket.Send(request);
                byte[] buffer = new byte[1];
                int length = conn.Socket.Receive(buffer);
                result = buffer[0];

                if (ERRORSUCCESS != result)
                {
                    //TODO: What should we do there?
                    yield break;
                }

                request[1] = (byte)Commands.IterNext;
                buffer = new byte[5];
                while (true)
                {
                    conn.Socket.Send(request);
                    length = conn.Socket.Receive(buffer);
                    if (length != 5)
                    {
                        break;
                    }

                    result = buffer[1];
                    if (ERRORSUCCESS != result)
                        break;
                    int keyLength = ToBigEndian(buffer, 1);
                    byte[] key = new byte[keyLength];
                    readData(key, conn, keyLength);
                    yield return key;
                }
            }
        }
        /// <summary>
        /// Method will iterate through the database searching for keys with a certain prefix.
        /// </summary>
        /// <param name="prefix">prefix to search for.</param>
        /// <param name="Max">Maximum number of records</param>
        /// <returns>IEnumerable containing keys</returns>
        public IEnumerable<byte[]> GetKeysWithPrefix(byte[] prefix, int Max)
        {
            if (IsNullOrEmpty(prefix)) throw new ArgumentNullException("prefix", "prefix cannot be null or empty.");
            const int HEADERLENGTH = 6;
            int sendBufferLength = HEADERLENGTH + prefix.Length + 4;
            byte[] sendBuffer = new byte[sendBufferLength];
            sendBuffer[0] = STARTBYTE;
            sendBuffer[1] = (byte)Commands.FWMKeys;
            int index = 2;
            writeInt32(sendBuffer, prefix.Length, ref index);
            writeInt32(sendBuffer, Max, ref index);
            copyArray(sendBuffer, prefix, ref index);
            byte result;
            using (Connection conn = ConnectionPool.GetConnection(EndPoint))
            {
                conn.Socket.Send(sendBuffer);

                byte[] buffer = new byte[5];
                int length = conn.Socket.Receive(buffer);
                result = buffer[0];
                if (ERRORSUCCESS != result)
                {

                }
                int TotalKeys = ToBigEndian(buffer, 1);
                int KeysProcessed = 0;
                while (TotalKeys>KeysProcessed)
                {
                    int keyLength = conn.Socket.Receive(buffer);
                    byte[] key = new byte[keyLength];
                    readData(key, conn, keyLength);
                    KeysProcessed++;
                    yield return key;
                }

            }
        }
        /// <summary>
        /// Method is used to increment an item by an integer value.
        /// </summary>
        /// <param name="key">Key to increment</param>
        /// <param name="value">value to increment by</param>
        /// <returns>Current Value</returns>
        public int Increment(byte[] key, int value)
        {
            if (IsNullOrEmpty(key)) throw new ArgumentNullException("key", "key cannot be null or empty.");
            const int HEADERLENGTH = 6;
            int sendBufferLength = HEADERLENGTH + key.Length + 4;
            byte[] sendBuffer = new byte[sendBufferLength];
            sendBuffer[0] = STARTBYTE;
            sendBuffer[1] = (byte)Commands.AddInt;
            int index = 2;
            writeInt32(sendBuffer, key.Length, ref index);
            writeInt32(sendBuffer, value, ref index);
            copyArray(sendBuffer, key, ref index);

            byte result;
            using (Connection conn = ConnectionPool.GetConnection(EndPoint))
            {
                conn.Socket.Send(sendBuffer);
                byte[] buffer = new byte[5];
                int length = conn.Socket.Receive(buffer);
                result = buffer[0];

                if (ERRORSUCCESS != result)
                {
                    return -1;
                }

                int currentValue = ToBigEndian(buffer, 1);
                return currentValue;
            }
        }
        /// <summary>
        /// Method forces a write to disk.
        /// </summary>
        public void Sync()
        {
            const int HEADERLENGTH = 2;
            int sendBufferLength = HEADERLENGTH;
            byte[] sendBuffer = new byte[sendBufferLength];
            sendBuffer[0] = STARTBYTE;
            sendBuffer[1] = (byte)Commands.Sync;
            byte result;
            using (Connection conn = ConnectionPool.GetConnection(EndPoint))
            {
                conn.Socket.Send(sendBuffer);
                byte[] buffer = new byte[5];
                int length = conn.Socket.Receive(buffer);
                result = buffer[0];

                if (ERRORSUCCESS != result)
                {
                    
                }
            }
        }
        /// <summary>
        /// Method is used to optimize the database
        /// </summary>
        /// <param name="parameters">parameters to optimize the database with</param>
        public void Optimize(string parameters)
        {
            if (string.IsNullOrEmpty(parameters)) throw new ArgumentNullException("parameters", "parameters cannot be null or empty.");
            const int HEADERLENGTH = 2;
            int sendBufferLength = HEADERLENGTH;
            byte[] paramBuffer = Encoding.ASCII.GetBytes(parameters);

            byte[] sendBuffer = new byte[sendBufferLength];
            int index=2;
            sendBuffer[0] = STARTBYTE;
            sendBuffer[1] = (byte)Commands.AddInt;
            writeInt32(sendBuffer, paramBuffer.Length, ref index);
            copyArray(sendBuffer, paramBuffer, ref index);
            byte result;
            using (Connection conn = ConnectionPool.GetConnection(EndPoint))
            {
                conn.Socket.Send(sendBuffer);
                byte[] buffer = new byte[1];
                int length = conn.Socket.Receive(buffer);
                result = buffer[0];

                if (ERRORSUCCESS != result)
                {

                }
            }

        }
        /// <summary>
        /// Method is used to clear the database.
        /// </summary>
        public void Vanish()
        {
            const int HEADERLENGTH = 2;
            int sendBufferLength = HEADERLENGTH;
            byte[] sendBuffer = new byte[sendBufferLength];
            sendBuffer[0] = STARTBYTE;
            sendBuffer[1] = (byte)Commands.Vanish;
            byte result;
            using (Connection conn = ConnectionPool.GetConnection(EndPoint))
            {
                conn.Socket.Send(sendBuffer);
                byte[] buffer = new byte[1];
                int length = conn.Socket.Receive(buffer);
                result = buffer[0];

                if (ERRORSUCCESS != result)
                {

                }
            }


        }

        /// <summary>
        /// Method is used to check an array to see if it is null or empty.
        /// </summary>
        /// <param name="array">Array to check.</param>
        /// <returns>True if Null Or Empty.</returns>
        private static bool IsNullOrEmpty(Array array)
        {
            return null == array || array.Length == 0;
        }
        /// <summary>
        /// Method is used to read a specific amount of data off of the wire. 
        /// </summary>
        /// <param name="data">buffer to write to</param>
        /// <param name="conn">connection to read from</param>
        /// <param name="length">amount of data to read</param>
        private static void readData(byte[] buffer, Connection conn, int length)
        {
            int totalread = 0;
            while (totalread < length)
            {
                int dataNeeded = length - totalread;
                int read = conn.Socket.Receive(buffer, totalread, dataNeeded, SocketFlags.None);
                totalread += read;
            }
        }
        /// <summary>
        /// Method is used to put data in the database.
        /// </summary>
        /// <param name="key">key used to store the data under.</param>
        /// <param name="value">Value to store</param>
        /// <param name="command">Specific command to use.</param>
        /// <returns>True if the data was stored successfully.</returns>
        private bool Put(byte[] key, byte[] value, Commands command)
        {
            if (IsNullOrEmpty(key)) throw new ArgumentNullException("key", "key cannot be null or empty.");
            if (IsNullOrEmpty(value)) throw new ArgumentNullException("value", "value cannot be null or empty.");

            const int HEADERLENGTH=10;
            int sendBufferLength=HEADERLENGTH + key.Length + value.Length;
            byte[] sendBuffer=new byte[sendBufferLength];
            sendBuffer[0] = STARTBYTE;
            sendBuffer[1] = (byte)command;
            int index = 2;
            writeInt32(sendBuffer, key.Length, ref index);
            writeInt32(sendBuffer, value.Length, ref index);
            copyArray(sendBuffer, key, ref index);
            copyArray(sendBuffer, value, ref index);
            byte result;
            using (Connection conn = ConnectionPool.GetConnection(EndPoint))
            {
                conn.Socket.Send(sendBuffer);

                if (Commands.PutNR == command)
                    return true;
                byte[] buffer = new byte[1];
                int length = conn.Socket.Receive(buffer);
                result = buffer[0];
                return ERRORSUCCESS == result;
            }
            
        }
        /// <summary>
        /// Method is used to write an integer in the proper format to the supplied buffer
        /// </summary>
        /// <param name="buffer">buffer to write to</param>
        /// <param name="value">value to write</param>
        /// <param name="index">Index to start at.</param>
        private void writeInt32(byte[] buffer, int value, ref int index)
        {
            int bigedian = ToBigEndian(value);
            byte[] cbuffer = BitConverter.GetBytes(bigedian);
            Array.Copy(cbuffer, 0, buffer, index, cbuffer.Length);
            index += 4;
        }
        /// <summary>
        /// Method is used to write the value of an array to the buffer. 
        /// </summary>
        /// <param name="buffer">Buffer to write to.</param>
        /// <param name="source">Array to copy data from.</param>
        /// <param name="index">Index to start with.</param>
        private void copyArray(byte[] buffer, byte[] source, ref int index)
        {
            Array.Copy(source, 0, buffer, index, source.Length);
            index += source.Length;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private static int ToBigEndian(int value)
        {
            return IPAddress.HostToNetworkOrder(value);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        private static int ToBigEndian(byte[] buffer, int index)
        {
            int value = BitConverter.ToInt32(buffer, index);
            return ToBigEndian(value);
        }        
    }
}
